// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: jshin

#pragma once

#include <map>
#include <string>
#include <string_view>

#include <simdjson.h>

#include "coin/proto/coin_market_enums.pb.h"
#include "coin2/exchange/base/feed/update.h"
#include "coin2/exchange/base/feed_util/book_builder.h"
#include "coin2/exchange/base/feed_util/trade.h"
#include "coin2/exchange/base/feed_util/kline.h"
#include "coin2/exchange/huobi/feed_v1/update.h"

namespace impl {
using coin2::exchange::base::market::ExchangeType;
using coin2::exchange::base::market::MarketType;
using coin2::exchange::base::feed::FeedUpdateManager;

inline coin::proto::OrderSide GetOrderSide(
    const std::string_view direction,
    const std::string_view offset) {
  if (direction == "buy") {
    return coin::proto::OrderSide::BUY_CLOSE_ORDER;
  } else if (direction == "sell") {
    return coin::proto::OrderSide::SELL_CLOSE_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

class HuobiFuturesFeedUpdateParser {
 public:
  static void ParseBook(
      BookBuilder* book_builder_,
      const simdjson::dom::object& obj,
      bool enable_light_bbo) {
    auto ts = obj["ts"].get<int64_t>() * 1'000'000LL;  // ms -> ns
    auto tx = book_builder_->NewTransaction(ts);
    const auto& tick = obj["tick"];

    std::string_view event;
    auto err1 = tick["event"].get(event);
    if (err1 || event == "snapshot") {
      tx.Clear();
    } else if (enable_light_bbo) {
      tx.BboClear();
    }

    // Huobi sends a msg without 'asks', 'bids' field around rollover time
    simdjson::dom::array asks;
    auto err2 = tick["asks"].get(asks);
    if (!err2) {
      for (const auto& pq : asks) {
        tx.UpdateAsk(pq.at(0).get<double>(), pq.at(1).get<double>());
      }
    }
    simdjson::dom::array bids;
    auto err3 = tick["bids"].get(bids);
    if (!err3) {
      for (const auto& pq : bids) {
        tx.UpdateBid(pq.at(0).get<double>(), pq.at(1).get<double>());
      }
    }
  }
  static void ParseTrade(
      const simdjson::dom::element& trade_json,
      int qty_divisor,
      Trade* trade) {
    // trade_json->timestamp is reused
    trade->price = trade_json["price"].get<double>();
    trade->fill_qty = trade_json["amount"].get<double>() / qty_divisor;
    trade->side = ParseTradeSide(trade_json["direction"].get<std::string_view>());
  }

  static coin2::exchange::feed::TradeSide ParseTradeSide(
      std::string_view side) {
    if (side == "buy") {
      return coin2::exchange::feed::TRADE_BUY_SIDE;
    } else if (side == "sell") {
      return coin2::exchange::feed::TRADE_SELL_SIDE;
    } else if (side.empty()) {
      return coin2::exchange::feed::TRADE_NO_SIDE;
    } else {
      throw std::runtime_error("[HuobiCommon] unknown side: " + std::string(side));
    }
  }

  static void ParseLiquidationOrder(
      const simdjson::dom::element& msg,
      LiquidationOrder* liquidation_order) {
    CHECK(liquidation_order);
    liquidation_order->Clear();
    liquidation_order->timestamp = msg["created_at"].get<int64_t>() * 1'000'000LL;
    liquidation_order->price = msg["price"].get<double>();
    liquidation_order->qty = msg["volume"].get<double>();
    liquidation_order->order_side = GetOrderSide(
        msg["direction"].get<std::string_view>(), msg["offset"].get<std::string_view>());
  }

  static void ParseFundingRate(
      const simdjson::dom::element& msg,
      FundingRate* funding_rate,
      int64_t timestamp) {
    CHECK(funding_rate);
    funding_rate->Clear();
    funding_rate->timestamp = timestamp;
    funding_rate->funding_rate = stod(msg["funding_rate"].get<std::string_view>());
    // In Huobi funding rate WS message, 'funding_time' field gives
    // current timestamp.
    // In according restful query case, it gives correct funding time.
    // Since settlement_time gives the same time as 'funding_time',
    // we can use settlement_time instead.
    funding_rate->funding_time =
       stoll(msg["settlement_time"].get<std::string_view>()) * 1'000'000LL;
    funding_rate->estimated_rate = stod(msg["estimated_rate"].get<std::string_view>());
  }

  static void ParseBbo(
      BookBuilder* book_builder_,
      const simdjson::dom::object& obj) {
    auto ts = obj["ts"].get<int64_t>() * 1'000'000LL;  // ms -> ns
    auto tx = book_builder_->NewTransaction(ts);
    tx.Clear();
    const auto& tick = obj["tick"];

    // Huobi sends a msg without 'asks', 'bids' field around rollover time
    simdjson::dom::array ask;
    auto err1 = tick["ask"].get(ask);
    if (!err1) {
      tx.UpdateAsk(ask.at(0).get<double>(), ask.at(1).get<double>());
    }
    simdjson::dom::array bid;
    auto err2 = tick["bid"].get(bid);
    if (!err2) {
      tx.UpdateBid(bid.at(0).get<double>(), bid.at(1).get<double>());
    }
  }

  static bool ParseKline(
      Klines* klines,
      const simdjson::dom::object& obj,
      fastfeed::proto::KlineInterval interval,
      int64_t ts) {
    int64_t open_timestamp = obj["id"].get<int64_t>() * 1'000'000'000LL;
    auto result = klines->AddKline(open_timestamp);
    if (!result) return false;

    Kline* kline;
    bool freezed;
    std::tie(kline, freezed) = result.value();

    // huobi publish unfreezed kline, but we only publish freezed kline
    HuobiFuturesFeedUpdateParser::Parse1Kline(kline, obj, interval, ts, open_timestamp);
    return freezed;
  }

  static void Parse1Kline(
      Kline* kline,
      const simdjson::dom::object& obj,
      fastfeed::proto::KlineInterval interval,
      int64_t ts,
      int64_t open_timestamp) {
    kline->open_timestamp = open_timestamp;
    kline->open = obj["open"].get<double>();
    kline->high = obj["high"].get<double>();
    kline->low = obj["low"].get<double>();
    kline->close = obj["close"].get<double>();
    kline->volume = obj["vol"].get<double>() / 2;  // both buy and sell sides
    simdjson::error_code err;
    double trade_turnover = 0;
    obj["trade_turnover"].get<double>().tie(trade_turnover, err);
    if (err != simdjson::NO_SUCH_FIELD) {
      kline->turnover = trade_turnover / 2;  // both buy and sell sides for linear swap
    } else {
      double amount = 0;
      obj["amount"].get<double>().tie(amount, err);
      if (err != simdjson::NO_SUCH_FIELD) {
        kline->turnover = amount / 2; // both buy and sell sides
      }
    }
    kline->timestamp = ts;
    kline->interval = interval;
  }
};

}  // namespace impl

using impl::HuobiFuturesFeedUpdateParser;
